// Copyright IBM Corp. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package localconfig

import (
	"fmt"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/go-viper/mapstructure/v2"
	"github.com/hyperledger/fabric/core/config/configtest"
	"github.com/stretchr/testify/require"
)

func TestLoadGoodConfig(t *testing.T) {
	configtest.SetDevFabricConfigPath(t)
	cc := &configCache{}
	cfg, err := cc.load()
	require.NoError(t, err)
	require.NotNil(t, cfg, "Could not load config")
	require.Nil(t, err, "Load good config returned unexpected error")
}

func TestMissingConfigValueOverridden(t *testing.T) {
	t.Run("when the value is missing and not overridden", func(t *testing.T) {
		configtest.SetDevFabricConfigPath(t)
		cc := &configCache{}
		cfg, err := cc.load()
		require.NotNil(t, cfg, "Could not load config")
		require.NoError(t, err, "Load good config returned unexpected error")
		require.Nil(t, cfg.General.TLS.ClientRootCAs)
	})

	t.Run("when the value is missing and is overridden", func(t *testing.T) {
		t.Setenv("ORDERER_GENERAL_TLS_CLIENTROOTCAS", "msp/tlscacerts/tlsroot.pem")
		configtest.SetDevFabricConfigPath(t)
		cache := &configCache{}
		cfg, err := cache.load()
		require.NotNil(t, cfg, "Could not load config")
		require.NoError(t, err, "Load good config returned unexpected error")
		require.NotNil(t, cfg.Admin.TLS.ClientRootCAs)
	})
}

func TestLoadCached(t *testing.T) {
	configtest.SetDevFabricConfigPath(t)

	// Load the initial config, update the environment, and load again.
	// With the caching behavior, the update should not be reflected
	initial, err := Load()
	require.NoError(t, err)
	t.Setenv("ORDERER_GENERAL_KEEPALIVE_SERVERTIMEOUT", "120s")
	updated, err := Load()
	require.NoError(t, err)
	require.Equal(t, initial, updated, "expected %#v to equal %#v", updated, initial)

	// Change the configuration we got back and load again.
	// The new value should not contain the update to the initial
	initial.General.LocalMSPDir = "/test/bad/mspDir"
	updated, err = Load()
	require.NoError(t, err)
	require.NotEqual(t, initial, updated, "expected %#v to not equal %#v", updated, initial)
}

func TestLoadMissingConfigFile(t *testing.T) {
	t.Setenv("FABRIC_CFG_PATH", "invalid fabric cfg path")

	cc := &configCache{}
	cfg, err := cc.load()
	require.Nil(t, cfg, "Loaded missing config file")
	require.NotNil(t, err, "Loaded missing config file without error")
}

func TestLoadMalformedConfigFile(t *testing.T) {
	name := t.TempDir()

	// Create a malformed orderer.yaml file in temp dir
	f, err := os.OpenFile(filepath.Join(name, "orderer.yaml"), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o600)
	require.Nil(t, err, "Error creating file: %s", err)
	f.WriteString("General: 42")
	require.NoError(t, f.Close(), "Error closing file")

	t.Setenv("FABRIC_CFG_PATH", name)

	cc := &configCache{}
	cfg, err := cc.load()
	require.Nil(t, cfg, "Loaded missing config file")
	require.NotNil(t, err, "Loaded missing config file without error")
}

// TestEnvInnerVar verifies that with the Unmarshal function that
// the environmental overrides still work on internal vars.  This was
// a bug in the original viper implementation that is worked around in
// the Load codepath for now
func TestEnvInnerVar(t *testing.T) {
	envVar1 := "ORDERER_GENERAL_LISTENPORT"
	envVal1 := uint16(80)
	envVar2 := "ORDERER_GENERAL_KEEPALIVE_SERVERTIMEOUT"
	envVal2 := "42s"
	t.Setenv(envVar1, fmt.Sprintf("%d", envVal1))
	t.Setenv(envVar2, envVal2)
	configtest.SetDevFabricConfigPath(t)

	cc := &configCache{}
	config, err := cc.load()
	require.NoError(t, err)

	require.NotNil(t, config, "Could not load config")
	require.Equal(t, config.General.ListenPort, envVal1, "Environmental override of inner config test 1 did not work")

	v2, _ := time.ParseDuration(envVal2)
	require.Equal(t, config.General.Keepalive.ServerTimeout, v2, "Environmental override of inner config test 2 did not work")
}

func TestAdminTLSConfig(t *testing.T) {
	testCases := []struct {
		name        string
		tls         TLS
		shouldPanic bool
	}{
		{
			name: "no TLS",
			tls: TLS{
				Enabled:            false,
				ClientAuthRequired: false,
			},
			shouldPanic: false,
		},
		{
			name: "TLS enabled and ClientAuthRequired",
			tls: TLS{
				Enabled:            true,
				ClientAuthRequired: true,
			},
			shouldPanic: false,
		},
		{
			name: "TLS enabled and ClientAuthRequired set to false",
			tls: TLS{
				Enabled:            true,
				ClientAuthRequired: false,
			},
			shouldPanic: true,
		},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			uconf := &TopLevel{Admin: Admin{TLS: tc.tls}}
			if tc.shouldPanic {
				require.PanicsWithValue(t, "Admin.TLS.ClientAuthRequired must be set to true if Admin.TLS.Enabled is set to true", func() { uconf.completeInitialization("/dummy/path") })
			} else {
				require.NotPanics(t, func() { uconf.completeInitialization("/dummy/path") }, "Should not panic")
			}
		})
	}
}

func TestClusterDefaults(t *testing.T) {
	configtest.SetDevFabricConfigPath(t)

	cc := &configCache{}
	cfg, err := cc.load()
	require.NoError(t, err)
	require.Equal(t, cfg.General.Cluster.ReplicationMaxRetries, Defaults.General.Cluster.ReplicationMaxRetries)
	require.Equal(t, cfg.General.Cluster.ReplicationPolicy, Defaults.General.Cluster.ReplicationPolicy)
}

func TestConsensusConfig(t *testing.T) {
	name := t.TempDir()

	content := `---
Consensus:
  Foo: bar
  Hello:
    World: 42
`

	f, err := os.OpenFile(filepath.Join(name, "orderer.yaml"), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o600)
	require.Nil(t, err, "Error creating file: %s", err)
	f.WriteString(content)
	require.NoError(t, f.Close(), "Error closing file")

	t.Setenv("FABRIC_CFG_PATH", name)

	cc := &configCache{}
	conf, err := cc.load()
	require.NoError(t, err, "Load good config returned unexpected error")
	require.NotNil(t, conf, "Could not load config")

	consensus := conf.Consensus
	require.IsType(t, map[string]interface{}{}, consensus, "Expected Consensus to be of type map[string]interface{}")

	foo := &struct {
		Foo   string
		Hello struct {
			World int
		}
	}{}
	err = mapstructure.Decode(consensus, foo)
	require.NoError(t, err, "Failed to decode Consensus to struct")
	require.Equal(t, foo.Foo, "bar")
	require.Equal(t, foo.Hello.World, 42)
}

func TestConnectionTimeout(t *testing.T) {
	t.Run("without connection timeout overridden", func(t *testing.T) {
		configtest.SetDevFabricConfigPath(t)
		cc := &configCache{}
		cfg, err := cc.load()
		require.NotNil(t, cfg, "Could not load config")
		require.NoError(t, err, "Load good config returned unexpected error")
		require.Equal(t, cfg.General.ConnectionTimeout, time.Duration(0))
	})

	t.Run("with connection timeout overridden", func(t *testing.T) {
		t.Setenv("ORDERER_GENERAL_CONNECTIONTIMEOUT", "10s")
		configtest.SetDevFabricConfigPath(t)

		cc := &configCache{}
		cfg, err := cc.load()
		require.NotNil(t, cfg, "Could not load config")
		require.NoError(t, err, "Load good config returned unexpected error")
		require.Equal(t, cfg.General.ConnectionTimeout, 10*time.Second)
	})
}

func TestChannelParticipationDefaults(t *testing.T) {
	configtest.SetDevFabricConfigPath(t)

	cc := &configCache{}
	cfg, err := cc.load()
	require.NoError(t, err)
	require.Equal(t, cfg.ChannelParticipation.Enabled, Defaults.ChannelParticipation.Enabled)
	require.Equal(t, cfg.ChannelParticipation.MaxRequestBodySize, Defaults.ChannelParticipation.MaxRequestBodySize)
}
